/*
 * Copyright (C) 2007-2012 by XDEV Software, All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 3.0 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
 
package xdev.util;


import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Vector;


/**
 * Largely GNU-compatible command-line options parser. Has short (-v) and
 * long-form (--verbose) option support, and also allows options with associated
 * values (-d 2, --debug 2, --debug=2). Option processing can be explicitly
 * terminated by the argument '--'.
 */
public class CmdLineParser
{
	/**
	 * Base class for exceptions that may be thrown when options are parsed.
	 */
	public static abstract class OptionException extends Exception
	{
		OptionException(String msg)
		{
			super(msg);
		}
	}


	/**
	 * Thrown when the parsed command-line contains an option that is not
	 * recognised. <code>getMessage()</code> returns an error string suitable
	 * for reporting the error to the user (in English).
	 */
	public static class UnknownOptionException extends OptionException
	{
		UnknownOptionException(String optionName)
		{
			this(optionName,"Unknown option '" + optionName + "'");
		}


		UnknownOptionException(String optionName, String msg)
		{
			super(msg);
			this.optionName = optionName;
		}


		/**
		 * @return the name of the option that was unknown (e.g. "-u")
		 */
		public String getOptionName()
		{
			return this.optionName;
		}
		private String	optionName	= null;
	}


	/**
	 * Thrown when the parsed commandline contains multiple concatenated short
	 * options, such as -abcd, where one is unknown. <code>getMessage()</code>
	 * returns an english human-readable error string.
	 * 
	 * @author Vidar Holen
	 */
	public static class UnknownSuboptionException extends UnknownOptionException
	{
		private char	suboption;


		UnknownSuboptionException(String option, char suboption)
		{
			super(option,"Illegal option: '" + suboption + "' in '" + option + "'");
			this.suboption = suboption;
		}


		public char getSuboption()
		{
			return suboption;
		}
	}


	/**
	 * Thrown when the parsed commandline contains multiple concatenated short
	 * options, such as -abcd, where one or more requires a value.
	 * <code>getMessage()</code> returns an english human-readable error string.
	 * 
	 * @author Vidar Holen
	 */
	public static class NotFlagException extends UnknownOptionException
	{
		private char	notflag;


		NotFlagException(String option, char unflaggish)
		{
			super(option,"Illegal option: '" + option + "', '" + unflaggish + "' requires a value");
			notflag = unflaggish;
		}


		/**
		 * @return the first character which wasn't a boolean (e.g 'c')
		 */
		public char getOptionChar()
		{
			return notflag;
		}
	}


	/**
	 * Thrown when an illegal or missing value is given by the user for an
	 * option that takes a value. <code>getMessage()</code> returns an error
	 * string suitable for reporting the error to the user (in English).
	 */
	public static class IllegalOptionValueException extends OptionException
	{
		public IllegalOptionValueException(Option opt, String value)
		{
			super("Illegal value '" + value + "' for option "
					+ (opt.shortForm() != null ? "-" + opt.shortForm() + "/" : "") + "--"
					+ opt.longForm());
			this.option = opt;
			this.value = value;
		}


		/**
		 * @return the name of the option whose value was illegal (e.g. "-u")
		 */
		public Option getOption()
		{
			return this.option;
		}


		/**
		 * @return the illegal value
		 */
		public String getValue()
		{
			return this.value;
		}
		private Option	option;
		private String	value;
	}


	/**
	 * Representation of a command-line option.
	 */
	public static abstract class Option
	{

		protected Option(String longForm, boolean wantsValue)
		{
			this(null,longForm,wantsValue);
		}


		protected Option(char shortForm, String longForm, boolean wantsValue)
		{
			this(new String(new char[]{shortForm}),longForm,wantsValue);
		}


		private Option(String shortForm, String longForm, boolean wantsValue)
		{
			if(longForm == null)
				throw new IllegalArgumentException("Null longForm not allowed");
			this.shortForm = shortForm;
			this.longForm = longForm;
			this.wantsValue = wantsValue;
		}


		public String shortForm()
		{
			return this.shortForm;
		}


		public String longForm()
		{
			return this.longForm;
		}


		/**
		 * Tells whether or not this option wants a value.
		 */
		public boolean wantsValue()
		{
			return this.wantsValue;
		}


		public final Object getValue(String arg, Locale locale) throws IllegalOptionValueException
		{
			if(this.wantsValue)
			{
				if(arg == null)
				{
					throw new IllegalOptionValueException(this,"");
				}
				return this.parseValue(arg,locale);
			}
			else
			{
				return Boolean.TRUE;
			}
		}


		/**
		 * Override to extract and convert an option value passed on the
		 * command-line.
		 */
		protected Object parseValue(String arg, Locale locale) throws IllegalOptionValueException
		{
			return null;
		}

		private String	shortForm	= null;
		private String	longForm	= null;
		private boolean	wantsValue	= false;


		public static class BooleanOption extends Option
		{
			public BooleanOption(char shortForm, String longForm)
			{
				super(shortForm,longForm,false);
			}


			public BooleanOption(String longForm)
			{
				super(longForm,false);
			}
		}


		/**
		 * An option that expects an integer value.
		 */
		public static class IntegerOption extends Option
		{
			public IntegerOption(char shortForm, String longForm)
			{
				super(shortForm,longForm,true);
			}


			public IntegerOption(String longForm)
			{
				super(longForm,true);
			}


			protected Object parseValue(String arg, Locale locale)
					throws IllegalOptionValueException
			{
				try
				{
					return new Integer(arg);
				}
				catch(NumberFormatException e)
				{
					throw new IllegalOptionValueException(this,arg);
				}
			}
		}


		/**
		 * An option that expects a long integer value.
		 */
		public static class LongOption extends Option
		{
			public LongOption(char shortForm, String longForm)
			{
				super(shortForm,longForm,true);
			}


			public LongOption(String longForm)
			{
				super(longForm,true);
			}


			protected Object parseValue(String arg, Locale locale)
					throws IllegalOptionValueException
			{
				try
				{
					return new Long(arg);
				}
				catch(NumberFormatException e)
				{
					throw new IllegalOptionValueException(this,arg);
				}
			}
		}


		/**
		 * An option that expects a floating-point value.
		 */
		public static class DoubleOption extends Option
		{
			public DoubleOption(char shortForm, String longForm)
			{
				super(shortForm,longForm,true);
			}


			public DoubleOption(String longForm)
			{
				super(longForm,true);
			}


			protected Object parseValue(String arg, Locale locale)
					throws IllegalOptionValueException
			{
				try
				{
					NumberFormat format = NumberFormat.getNumberInstance(locale);
					Number num = (Number)format.parse(arg);
					return new Double(num.doubleValue());
				}
				catch(ParseException e)
				{
					throw new IllegalOptionValueException(this,arg);
				}
			}
		}


		/**
		 * An option that expects a string value.
		 */
		public static class StringOption extends Option
		{
			public StringOption(char shortForm, String longForm)
			{
				super(shortForm,longForm,true);
			}


			public StringOption(String longForm)
			{
				super(longForm,true);
			}


			protected Object parseValue(String arg, Locale locale)
			{
				return arg;
			}
		}
	}


	/**
	 * Add the specified Option to the list of accepted options.
	 */
	public final Option addOption(Option opt)
	{
		if(opt.shortForm() != null)
			this.options.put("-" + opt.shortForm(),opt);
		this.options.put("--" + opt.longForm(),opt);
		return opt;
	}


	/**
	 * Convenience method for adding a string option.
	 * 
	 * @return the new Option
	 */
	public final Option addStringOption(char shortForm, String longForm)
	{
		return addOption(new Option.StringOption(shortForm,longForm));
	}


	/**
	 * Convenience method for adding a string option.
	 * 
	 * @return the new Option
	 */
	public final Option addStringOption(String longForm)
	{
		return addOption(new Option.StringOption(longForm));
	}


	/**
	 * Convenience method for adding an integer option.
	 * 
	 * @return the new Option
	 */
	public final Option addIntegerOption(char shortForm, String longForm)
	{
		return addOption(new Option.IntegerOption(shortForm,longForm));
	}


	/**
	 * Convenience method for adding an integer option.
	 * 
	 * @return the new Option
	 */
	public final Option addIntegerOption(String longForm)
	{
		return addOption(new Option.IntegerOption(longForm));
	}


	/**
	 * Convenience method for adding a long integer option.
	 * 
	 * @return the new Option
	 */
	public final Option addLongOption(char shortForm, String longForm)
	{
		return addOption(new Option.LongOption(shortForm,longForm));
	}


	/**
	 * Convenience method for adding a long integer option.
	 * 
	 * @return the new Option
	 */
	public final Option addLongOption(String longForm)
	{
		return addOption(new Option.LongOption(longForm));
	}


	/**
	 * Convenience method for adding a double option.
	 * 
	 * @return the new Option
	 */
	public final Option addDoubleOption(char shortForm, String longForm)
	{
		return addOption(new Option.DoubleOption(shortForm,longForm));
	}


	/**
	 * Convenience method for adding a double option.
	 * 
	 * @return the new Option
	 */
	public final Option addDoubleOption(String longForm)
	{
		return addOption(new Option.DoubleOption(longForm));
	}


	/**
	 * Convenience method for adding a boolean option.
	 * 
	 * @return the new Option
	 */
	public final Option addBooleanOption(char shortForm, String longForm)
	{
		return addOption(new Option.BooleanOption(shortForm,longForm));
	}


	/**
	 * Convenience method for adding a boolean option.
	 * 
	 * @return the new Option
	 */
	public final Option addBooleanOption(String longForm)
	{
		return addOption(new Option.BooleanOption(longForm));
	}


	/**
	 * Equivalent to {@link #getOptionValue(Option, Object) getOptionValue(o,
	 * null)}.
	 */
	public final Object getOptionValue(Option o)
	{
		return getOptionValue(o,null);
	}


	/**
	 * @return the parsed value of the given Option, or null if the option was
	 *         not set
	 */
	public final Object getOptionValue(Option o, Object def)
	{
		Vector v = (Vector)values.get(o.longForm());

		if(v == null)
		{
			return def;
		}
		else if(v.isEmpty())
		{
			return null;
		}
		else
		{
			Object result = v.elementAt(0);
			v.removeElementAt(0);
			return result;
		}
	}


	/**
	 * @return A Vector giving the parsed values of all the occurrences of the
	 *         given Option, or an empty Vector if the option was not set.
	 */
	public final Vector getOptionValues(Option option)
	{
		Vector result = new Vector();

		while(true)
		{
			Object o = getOptionValue(option,null);

			if(o == null)
			{
				return result;
			}
			else
			{
				result.addElement(o);
			}
		}
	}


	/**
	 * @return the non-option arguments
	 */
	public final String[] getRemainingArgs()
	{
		return this.remainingArgs;
	}


	/**
	 * Extract the options and non-option arguments from the given list of
	 * command-line arguments. The default locale is used for parsing options
	 * whose values might be locale-specific.
	 */
	public final void parse(String[] argv) throws IllegalOptionValueException,
			UnknownOptionException
	{

		// It would be best if this method only threw OptionException, but for
		// backwards compatibility with old user code we throw the two
		// exceptions above instead.

		parse(argv,Locale.getDefault());
	}


	/**
	 * Extract the options and non-option arguments from the given list of
	 * command-line arguments. The specified locale is used for parsing options
	 * whose values might be locale-specific.
	 */
	public final void parse(String[] argv, Locale locale) throws IllegalOptionValueException,
			UnknownOptionException
	{

		// It would be best if this method only threw OptionException, but for
		// backwards compatibility with old user code we throw the two
		// exceptions above instead.

		Vector otherArgs = new Vector();
		int position = 0;
		this.values = new Hashtable(10);
		while(position < argv.length)
		{
			String curArg = argv[position];
			if(curArg.startsWith("-"))
			{
				if(curArg.equals("--"))
				{ // end of options
					position += 1;
					break;
				}
				String valueArg = null;
				if(curArg.startsWith("--"))
				{ // handle --arg=value
					int equalsPos = curArg.indexOf("=");
					if(equalsPos != -1)
					{
						valueArg = curArg.substring(equalsPos + 1);
						curArg = curArg.substring(0,equalsPos);
					}
				}
				else if(curArg.length() > 2)
				{ // handle -abcd
					for(int i = 1; i < curArg.length(); i++)
					{
						Option opt = (Option)this.options.get("-" + curArg.charAt(i));
						if(opt == null)
							throw new UnknownSuboptionException(curArg,curArg.charAt(i));
						if(opt.wantsValue())
							throw new NotFlagException(curArg,curArg.charAt(i));
						addValue(opt,opt.getValue(null,locale));

					}
					position++;
					continue;
				}

				Option opt = (Option)this.options.get(curArg);
				if(opt == null)
				{
					throw new UnknownOptionException(curArg);
				}
				Object value = null;
				if(opt.wantsValue())
				{
					if(valueArg == null)
					{
						position += 1;
						if(position < argv.length)
						{
							valueArg = argv[position];
						}
					}
					value = opt.getValue(valueArg,locale);
				}
				else
				{
					value = opt.getValue(null,locale);
				}

				addValue(opt,value);

				position += 1;
			}
			else
			{
				otherArgs.addElement(curArg);
				position += 1;
			}
		}
		for(; position < argv.length; ++position)
		{
			otherArgs.addElement(argv[position]);
		}

		this.remainingArgs = new String[otherArgs.size()];
		otherArgs.copyInto(remainingArgs);
	}


	private void addValue(Option opt, Object value)
	{
		String lf = opt.longForm();

		Vector v = (Vector)values.get(lf);

		if(v == null)
		{
			v = new Vector();
			values.put(lf,v);
		}

		v.addElement(value);
	}

	private String[]	remainingArgs	= null;
	private Hashtable	options			= new Hashtable(10);
	private Hashtable	values			= new Hashtable(10);
}
